package pkg.andru.util;

import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;

import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory;

/**
 * some basic bitmap utils collection.
 * 
 * @author idiottiger
 * @version 1.0
 */
public final class BitmapUtils {

    static final String TAG = "BitmapUtils";

    static final int BUFFER_SIZE = 16384; // default buffer size 16k

    public static final int H_QUALITY = 100;

    public static final int M_QUALITY = 80;

    public static final int L_QUALITY = 50;

    static final int FLAG_STREAM = 1, FLAG_BYTE_ARRAY = 2;

    /**
     * decode bitmap use input stream
     * 
     * @param ins
     * @param options
     * @return
     * @throws IOException
     *             when decode, the stream close or another happen
     * @throws OutOfMemoryError
     *             when current system memory is low
     */
    public static Bitmap decodeBitmap(InputStream ins, BitmapFactory.Options options) throws IOException, OutOfMemoryError {
        if (ins == null) {
            return null;
        }
        if (options == null) {
            options = createDefaultOptions();
        }
        return BitmapFactory.decodeStream(new FlushedInputStream(ins), null, options);
    }

    /**
     * decode bitmap use byte array
     * 
     * @param ins
     * @param options
     * @return
     * @throws OutOfMemoryError
     *             when current system memory is low
     */
    public static Bitmap decodeBitmap(byte[] bytes, BitmapFactory.Options options) throws OutOfMemoryError {
        if (bytes == null) {
            return null;
        }
        if (options == null) {
            options = createDefaultOptions();
        }
        int length = bytes.length;
        return BitmapFactory.decodeByteArray(bytes, 0, length, options);
    }

    private static BitmapFactory.Options createDefaultOptions() {
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inDither = false;
        options.inJustDecodeBounds = false;
        return options;
    }

    /**
     * calculate the bitmap's in samesize.
     * 
     * @param ins
     * @param maxResolutionX
     * @param maxResolutionY
     * @return
     */
    public static int computeInSampleSize(InputStream ins, int maxResolutionX, int maxResolutionY) {
        return computeInSampleSize(ins, null, FLAG_STREAM, maxResolutionX, maxResolutionY);
    }

    /**
     * calculate the bitmap's in samesize.
     * 
     * @param bytes
     * @param maxResolutionX
     * @param maxResolutionY
     * @return
     */
    public static int computeInSampleSize(byte[] bytes, int maxResolutionX, int maxResolutionY) {
        return computeInSampleSize(null, bytes, FLAG_BYTE_ARRAY, maxResolutionX, maxResolutionY);
    }

    private static int computeInSampleSize(InputStream ins, byte[] bytes, int flag, int maxResolutionX, int maxResolutionY) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;

        if (flag == FLAG_STREAM && ins != null) {
            BitmapFactory.decodeStream(ins, null, options);
        } else if (flag == FLAG_BYTE_ARRAY && bytes != null) {
            int length = bytes.length;
            BitmapFactory.decodeByteArray(bytes, 0, length, options);
        } else {
            return -1;
        }

        if (options.outWidth == -1 || options.outHeight == -1)
            return -1;

        int maxNumOfPixels = maxResolutionX * maxResolutionY;
        int minSideLength = Math.min(maxResolutionX, maxResolutionY) / 2;

        return computeSampleSize(options, minSideLength, maxNumOfPixels);
    }

    private static int computeSampleSize(BitmapFactory.Options options, int minSideLength, int maxNumOfPixels) {
        int initialSize = computeInitialSampleSize(options, minSideLength, maxNumOfPixels);

        int roundedSize;
        if (initialSize <= 8) {
            roundedSize = 1;
            while (roundedSize < initialSize) {
                roundedSize <<= 1;
            }
        } else {
            roundedSize = (initialSize + 7) / 8 * 8;
        }

        return roundedSize;
    }

    private static int computeInitialSampleSize(BitmapFactory.Options options, int minSideLength, int maxNumOfPixels) {
        double w = options.outWidth;
        double h = options.outHeight;

        int lowerBound = (maxNumOfPixels == -1) ? 1 : (int) Math.ceil(Math.sqrt(w * h / maxNumOfPixels));
        int upperBound = (minSideLength == -1) ? 128 : (int) Math.min(Math.floor(w / minSideLength), Math.floor(h / minSideLength));

        if (upperBound < lowerBound) {
            // return the larger one when there is no overlapping zone.
            return lowerBound;
        }

        if ((maxNumOfPixels == -1) && (minSideLength == -1)) {
            return 1;
        } else if (minSideLength == -1) {
            return lowerBound;
        } else {
            return upperBound;
        }
    }

    /**
     * save the bitmap to file.
     * 
     * @param bitmap
     * @param file
     *            save file path
     * @param format
     *            compress format, must {@link CompressFormat#JPEG} or
     *            {@link CompressFormat#PNG}
     * @return true mean save bitmap right, otherwise faild
     */
    public static boolean saveBitmapToFile(Bitmap bitmap, String file, CompressFormat format) {
        boolean result = false;
        if (bitmap != null && !bitmap.isRecycled() && file != null && format != null) {
            BufferedOutputStream bos = null;
            try {
                bos = new BufferedOutputStream(new FileOutputStream(file), BUFFER_SIZE);
                result = bitmap.compress(format, M_QUALITY, bos);
            } catch (IOException e) {
                result = false;
                Log.e(TAG, "save bitmap to file error", e);
            } finally {
                if (bos != null) {
                    try {
                        bos.close();
                    } catch (IOException e) {
                    }
                }
            }
        }
        return result;
    }

    /**
     * to avoid the network slow and transform data not stable problem.
     * 
     * @author idiottiger
     */
    static class FlushedInputStream extends FilterInputStream {

        public FlushedInputStream(InputStream inputStream) {
            super(inputStream);
        }

        @Override
        public long skip(long n) throws IOException {
            long totalBytesSkipped = 0L;
            while (totalBytesSkipped < n) {
                long bytesSkipped = in.skip(n - totalBytesSkipped);
                if (bytesSkipped == 0L) {
                    int bytes = read();
                    if (bytes < 0) {
                        break; // we reached EOF
                    } else {
                        bytesSkipped = 1; // we read one byte
                    }
                }
                totalBytesSkipped += bytesSkipped;
            }
            return totalBytesSkipped;
        }
    }
}
